Add TypeScript definition generation and --types-output CLI option#23
Add TypeScript definition generation and --types-output CLI option#23
--types-output CLI option#23Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the Fireplan compiler by adding support for generating TypeScript definition files directly from the YAML schema. This feature aims to improve developer experience for NodeJS/Firebase consumers by providing type safety and better autocompletion for their data structures, making it easier to interact with Firebase data defined by Fireplan rules. The changes involve adding a new module for type conversion, extending the CLI with a new output option, and updating documentation to reflect these capabilities. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a valuable feature for generating TypeScript definitions from the Fireplan schema, complete with a new CLI option and documentation. However, a critical security vulnerability was identified in the TypeGenerator class where property names are not correctly escaped during code generation, potentially allowing for code injection. Additionally, there are a couple of high-severity issues: a potential bug in index.js involving a shallow clone that could lead to the type generator operating on a mutated schema, and in type_generator.js, the type inference logic is overly aggressive and may produce incorrect types for complex expressions. Addressing these points will significantly improve the robustness and security of this new feature.
index.js
Outdated
| if (!output) output = input.replace(/\.ya?ml$/, '') + '.json'; | ||
| const rawSource = fs.readFileSync(input, 'utf8'); | ||
| const source = jsyaml.load(rawSource, {filename: input, schema: jsyaml.DEFAULT_SAFE_SCHEMA}); | ||
| const sourceForTypes = typesOutput ? clone(source, false) : null; |
There was a problem hiding this comment.
The clone(source, false) performs a shallow clone. However, the transform function mutates the source object deeply (e.g., by modifying source.root and source.functions). This can lead to incorrect TypeScript type generation because generateTypes will receive a partially modified object. A deep clone is required here to ensure the type generator works on a pristine copy of the source.
| const sourceForTypes = typesOutput ? clone(source, false) : null; | |
| const sourceForTypes = typesOutput ? clone(source) : null; |
| inferExpressionType(expression) { | ||
| if (!_.isString(expression)) return 'unknown'; | ||
| const parsed = this.parseConstraint(expression); | ||
| const oneOfType = this.oneOfType(parsed.expression); | ||
| if (oneOfType) return oneOfType; | ||
| if (/\bstring\b/.test(parsed.expression)) return 'string'; | ||
| if (/\bnumber\b/.test(parsed.expression)) return 'number'; | ||
| if (/\bboolean\b/.test(parsed.expression)) return 'boolean'; | ||
| if (/\bany\b/.test(parsed.expression)) return 'unknown'; | ||
| return this.resolveFunctionReferenceType(parsed.expression); | ||
| } |
There was a problem hiding this comment.
The current implementation of inferExpressionType uses broad regular expressions (/\bstring\b/.test(...)) to detect types. This can lead to incorrect type inference. For example, an expression like next.foo === "a string value" would be incorrectly inferred as type string instead of unknown (since it's a complex boolean expression).
The logic should be more conservative. It should only infer a primitive type if the expression is just that type's name (e.g., string). The resolveFunctionReferenceType method already handles this case correctly for single-word expressions. The regex-based checks for string, number, boolean, and any are redundant and error-prone, and should be removed. The function should rely on oneOfType and resolveFunctionReferenceType for simple cases and fall back to unknown for anything more complex, as documented in the README.
inferExpressionType(expression) {
if (!_.isString(expression)) return 'unknown';
const parsed = this.parseConstraint(expression);
const expr = parsed.expression;
const oneOfType = this.oneOfType(expr);
if (oneOfType) return oneOfType;
const funcType = this.resolveFunctionReferenceType(expr);
if (funcType) return funcType;
return 'unknown';
}
type_generator.js
Outdated
| } | ||
|
|
||
| propertyName(key) { | ||
| return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : `'${key}'`; |
There was a problem hiding this comment.
The propertyName function fails to properly escape single quotes and backslashes when wrapping a property name in single quotes. An attacker who can control the keys in the input YAML file (e.g., through a malicious pull request to a repository using fireplan) can inject arbitrary TypeScript code into the generated output file. If this generated file is subsequently imported into a project or processed by tools that execute code from TypeScript files, it could lead to arbitrary code execution. Using JSON.stringify is a safer way to handle property names that are not valid identifiers, as it correctly escapes special characters and wraps the string in double quotes.
| return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : `'${key}'`; | |
| return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : JSON.stringify(key); |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f721b7e271
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
type_generator.js
Outdated
| const value = yaml[key]; | ||
| const constraint = _.isString(value) ? value : value && value['.value']; | ||
| return { | ||
| key: key.charAt(0) === '$' ? '$' : this.propertyName(key), |
There was a problem hiding this comment.
Strip encrypted key annotations before emitting TS properties
The generated type keys currently use raw YAML key names for non-wildcard children, so Fireplan annotations like /encrypted or /encrypted[...] are emitted as part of the property name instead of being removed. In schemas that rely on encrypted path annotations (which transformBranch strips before building runtime rules), the .d.ts no longer matches actual Firebase paths and will type-check against incorrect field names.
Useful? React with 👍 / 👎.
type_generator.js
Outdated
| } | ||
|
|
||
| propertyName(key) { | ||
| return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : `'${key}'`; |
There was a problem hiding this comment.
Escape quoted property names in generated type literals
propertyName wraps non-identifier keys in single quotes without escaping, so a valid Firebase key containing an apostrophe (for example owner'sName) produces invalid TypeScript syntax in the generated .d.ts. This causes downstream TS compilation/parsing failures for schemas with such keys.
Useful? React with 👍 / 👎.
620be1b to
7710d02
Compare
pkaminski
left a comment
There was a problem hiding this comment.
@pkaminski resolved 5 discussions.
Reviewable status: 0 of 5 files reviewed, all discussions resolved.
24cab12 to
d588b8a
Compare
|
+r:@snoack |
d588b8a to
019e804
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 019e8046b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
type_generator.js
Outdated
| rows.push(`${_.repeat(' ', indent + 2)}${key}${required ? '' : '?'}: ${type}`); | ||
| } | ||
| }); | ||
| if (node.moreAllowed) rows.push(`${_.repeat(' ', indent + 2)}[key: string]: any`); |
There was a problem hiding this comment.
Avoid emitting duplicate string index signatures
When an object contains both a wildcard child and .more: true, this always appends a second string index signature ([key: string]: any) even though typeString already emitted one for the wildcard entry, so the generated .d.ts contains two [...: string] signatures and fails TypeScript compilation (TS2374). This breaks the documented wildcard+.more scenario instead of widening the wildcard type.
Useful? React with 👍 / 👎.
| const rows = ['{']; | ||
| _.forEach(node.entries, entry => { | ||
| const type = this.typeString(entry.node, indent + 2); | ||
| const required = entry.required || entry.wildcard || type === 'any'; |
There was a problem hiding this comment.
Keep
any-typed properties optional unless required
This forces every property inferred as any to be required in the generated type, even when the schema does not use the required keyword. In Fireplan rules, child presence is only enforced via required, so schemas like foo: any are valid when foo is absent, but the emitted type incorrectly requires foo and causes false type errors for consumers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
All required controls is whether the emitted property has a ? suffix, and AFAIK there's no point in having an any-typed property be optional.
019e804 to
60f7c44
Compare
pkaminski
left a comment
There was a problem hiding this comment.
@pkaminski made 1 comment and resolved 2 discussions.
Reviewable status: 0 of 7 files reviewed, all discussions resolved (waiting on snoack).
| const rows = ['{']; | ||
| _.forEach(node.entries, entry => { | ||
| const type = this.typeString(entry.node, indent + 2); | ||
| const required = entry.required || entry.wildcard || type === 'any'; |
There was a problem hiding this comment.
All required controls is whether the emitted property has a ? suffix, and AFAIK there's no point in having an any-typed property be optional.
Motivation
.d.tsoutput to give NodeJS/Firebase consumers a typed view of the Fireplan schema.README.md.Description
type_generator.jsmodule implementingTypeGeneratorthat converts the YAML schema and simple function signatures into aFirebaseDataTypeScript type.generateTypesand extendingtransformFileto accept atypesOutputpath and write the.d.tsfile when provided.-t, --types-outputin thefireplanexecutable to specify the TypeScript output file.README.mdwith usage instructions and a summary of what the generated types cover and when they fall back tounknown.Testing
Codex Task
This change is